﻿/*  This file is part of TextEditorLibrary - a library for the programmatic manipulation of text.

    TextEditorLibrary is free software: you can redistribute it and/or modify it under the terms
    of the GNU Lesser General Public License as published by the Free Software Foundation, either
    version 3 of the License, or (at your option) any later version.

    TextEditorLibrary is distributed in the hope that it will be useful, but WITHOUT ANY 
    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
    PURPOSE.  See the GNU Lesser General Public License for more details at 
    <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace TextEditorLibrary
{

/// <summary>Defines a bookmark in a Text which moves with the text.</summary>
    public partial class Mark
    {
        private static int _IDs = 0;    // Number of registered Marks so far
        private static object _idLock = new object(); // Locking object to protect _IDs
        internal int _ID;               // Mark ID for debug

        internal Text _text;            // Text that the Mark is in
        internal TextBlock _block;      // Block that the Mark is in
        internal int _position;         // Offset from the start of the Block
        Pattern _pattern = null;        // Saved search pattern

    // Can't construct from the API - use Text.AddMark()
        internal Mark(Text text, bool invalidatable) : 
            this(text, text._blocks._first, 0, invalidatable) {}
        internal Mark(Mark mark, bool invalidatable) : 
            this(mark._text, mark._block, mark._position, invalidatable) {}

        internal Mark(Text text, TextBlock block, int position, bool invalidatable)
        {
            _ID = 0;
            _text = text;
            _block = block;
            _position = position;

        // If the mark is to be invalidatable, add it to the list of marks to be invalidated 
        // if the text changes
            if (invalidatable)
                _text.Invalidatable(this);
        }

        internal static int GetNextID()
        {
            lock (_idLock)
                return ++_IDs;
        }

    //---------------------------------------------------------------------------------------------

    // If a Mark is unregistered it will become invalid when the text changes, so check that.

        internal void Validate(string argument)
        {
            if (_ID < 0)
                throw new ArgumentException("Mark argument is invalid.", argument);
        }

        internal void ValidateThis()
        {
            if (_ID < 0)
                throw new InvalidOperationException("This Mark is invalid.");
        }

    // We often need the Mark to be in a specific text as well.

        internal void Validate(Text text, string argument)
        {
            if (_ID < 0)
                throw new ArgumentException("Mark argument is invalid.", argument);
            if (_text != text)
               throw new ArgumentException("Mark argument is in a different Text.", argument);
        }

    //---------------------------------------------------------------------------------------------

    /// <summary><para>Moves the Mark forwards or backwards in the Text.</para>
    /// <para>The Mark is returned to allow chaining of calls.</para></summary>
    /// <param name="offset">Number of characters, positive or negative, to move.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public Mark MoveTo(long offset)
        {
            ValidateThis();

            Mark mark = OffsetMark(offset,false);
            _block = mark._block;
            _position = mark._position;
            _pattern = null; // We cannot search again at the same location
            _text.Check();
            return this;
        }

    /// <summary><para>Moves the Mark to the position of a given mark value.</para>
    /// <para>The Mark is returned to allow chaining of calls.</para></summary>
    /// <param name="mark">Mark value defining the new position.</param>
    /// <exception cref="ArgumentException">The passed Mark has been invalidated or is in
    /// another Text.</exception>
        public Mark MoveTo(Mark mark)
        {
            mark.Validate(_text, "mark");

            _block = mark._block;
            _position = mark._position;
            _pattern = null; // We cannot search again at the same location

        // If the Mark was invalidated, then it becomes invalidatable again - usable until text 
        // changes
            if (_ID<0) _text.Invalidatable(this);

            _text.Check();
            return this;
        }

    /// <summary><para>Moves the Mark to the current cursor position in its Text.</para>
    /// <para>The Mark is returned to allow chaining of calls.</para></summary>
        public Mark MoveTo()
        {
            _block = _text._cursor._block;
            _position = _text._cursor._position;
            _pattern = null;

        // If the Mark was invalidated, then it becomes invalidatable again - usable until text 
        // changes
            if (_ID<0) _text.Invalidatable(this);

            _text.Check();
            return this;
        }
    
    //---------------------------------------------------------------------------------------------
    
    /// <summary><para>Inserts text at the Mark position.</para>
    /// <para>The Mark is moved to the end of the inserted text, and the Mark is returned to allow 
    /// chaining of calls. Other Marks originally at the same position are not moved.</para></summary>
    /// <param name="insert">string value containing the text for insertion.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public Mark Insert(string insert)
        {
            ValidateThis();

            TextBlock block = _block;
            int position = _position;
           
        // Chop the current block at the insertion point
            _text.Split(block, position);

        // Copy in as much text as will fit in the first block
            StringBuilder sb = block.GetData();
            int from = 0;
            int copy = Math.Min(Text.maxBlock-position, insert.Length);
            sb.Append(insert.Substring(from,copy));
            from += copy;

        // Add new blocks filling them up until we run out of text
            while (from < insert.Length)
            {
                copy = Math.Min(Text.tgtBlock, insert.Length-from);
                _text._blocks.AddAfter(block, new TextBlock(insert.Substring(from, copy)));
                from += copy;
                block = block._next;
            }

            _text._editLength += insert.Length;

        // Move the Mark to the end (all other Marks stay where they are)
            _block = block;
            _position = block._data.Length;

        // See if the last block can squeeze in with the one after (if any)
            _text.Join(block);

        // All unregistered marks and ranges are invalidated, except this one, because we know
        // it has been updated correctly
            _text.InvalidateAll();
            if (_ID < 0) _text.Invalidatable(this);

            _text.Check();

        // See if we have made any Boxes dirty - assume not many, all short
            foreach (Box box in _text._boxes)
            {
                if (!box.DirtyRange && box.TextLength != box.Range.Length()) box.OnRangeDirty();
            }

            return this;
        }

    /// <summary><para>Inserts text at the Mark position.</para>
    /// <para>The Mark is moved to the end of the inserted text, and the Mark is returned to allow 
    /// chaining of calls. Other Marks originally at the same position are not moved.</para></summary>
    /// <param name="source">object whose ToString method will define the text for insertion.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public Mark Insert(object source)
        {
            return Insert(source.ToString());
        }

    /// <summary><para>Deletes text forwards or backwards from the Mark position.</para>
    /// <para>The Mark is returned to allow chaining of calls.</para></summary>
    /// <param name="chars">Number of characters to delete, negative for deletion backwards,
    /// and positive for deletion forwards.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public Mark Delete(long chars)
        {
            ValidateThis();

            if (chars>0)
            {
                (new Range(this, OffsetMark(chars,false))).DoDelete();
                _pattern = null;
            }
            else if (chars<0)
            {
                (new Range(OffsetMark(chars,false), this)).DoDelete();
            }

        // We know this Mark has been updated correctly, so don't invalidate it
            _text.InvalidateAll();
            if (_ID < 0) _text.Invalidatable(this);
            return this;
        }

    /// <summary>Deletes text forwards or backwards from the Mark position, returning 
    /// the deleted text as a string.</summary>
    /// <param name="chars">Number of characters to delete, negative for deletion backwards,
    /// and positive for deletion forwards.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public string Cut(int chars)
        {
            ValidateThis();

            if (chars>0)
            {
                _pattern = null;
                return (new Range(this, OffsetMark(chars,false))).Cut();
            }
            else if (chars<0)
            {
                return (new Range(OffsetMark(chars,false), this)).Cut();
            }
            else
                return "";
        }

    //---------------------------------------------------------------------------------------------

    /// <summary>Returns the offset of this Mark from the passed Mark (positive if this Mark is
    /// after the passed Mark).</summary>
    /// <param name="mark">Mark value to compare this Mark to.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
    /// <exception cref="ArgumentException">The passed Mark has been invalidated.</exception>
        public long Offset(Mark mark)
        {
            ValidateThis();
            mark.Validate(_text, "mark");

            if (_block == mark._block)
                return (_position - mark._position);

        // Search in both directions together assuming that the Marks are likely to be close 
        // to each other but could be in either order
            TextBlock blockb = _block._previous;
            long offsetb = _position;
            TextBlock blocka = _block._next;  // Don't need to use GetNext - if mark is after, block will be there
            long offseta = _position - _block.GetLength();

            do
            {
                if (blockb != null)
                {
                    offsetb += blockb.GetLength();
                    if (blockb == mark._block)
                        return offsetb - mark._position;
                    blockb = blockb._previous;
                }

                if (blocka != null)
                {
                    if (blocka == mark._block)
                        return offseta - mark._position;
                    
                    offseta -= blocka.GetLength();
                    blocka = blocka._next;
                }
            } while (true);
        }

    /// <summary>Returns the offset of the first Mark from the second Mark.</summary>
    /// <param name="mark1">Mark value whose offset is returned.</param>
    /// <param name="mark2">Mark value used as the base for the offset.</param>
    /// <exception cref="ArgumentException">At least one of the Marks has been invalidated, or
    /// the Marks are in different Texts.</exception>
        public static long operator -(Mark mark1, Mark mark2)
        {
            mark1.Validate("mark1");
            mark2.Validate(mark1._text, "mark2");

            return mark1.Offset(mark2);
        }

    /// <summary>Returns the offset of this Mark from the start of its Text.</summary>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
    // Difficult to make this super-efficient, unless we were to create a hierarchy of
    // linked lists of linked lists so we only ever have to check (and update) O(log)
    // lengths.  But then we would have to worry about rebalancing those lists ...
        public long Offset()
        {
            ValidateThis();

            long offset = _position;
            TextBlock block = _text._blocks._first;
            while (block != _block)
            {
                offset += block.GetLength();
                block = block._next;
            }
            return offset;
        }

    /// <summary>Returns true if the passed Mark is at the same location as this Mark.</summary>
    /// <param name="mark">Mark to compare against.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
    /// <exception cref="ArgumentException">The passed Mark has been invalidated.</exception>
        public bool SameAs(Mark mark)
        {
            ValidateThis();
            mark.Validate(_text, "mark");

            return (mark._block==_block && mark._position==_position);
        }

    //---------------------------------------------------------------------------------------------
   
    /// <summary><para>Returns an unregistered Mark whose position is an offset from the Mark.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
    /// <param name="mark">Mark to start from.</param>
    /// <param name="offset">Number of characters, positive or negative, of offset.</param>
    /// <exception cref="ArgumentException">The Mark has been invalidated.</exception>
        public static Mark operator +(Mark mark, long offset)
        {
            mark.Validate("mark");
            return mark.OffsetMark(offset, true);
        }

    /// <summary><para>Returns an unregistered Mark whose position is an offset from the Mark.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
    /// <param name="mark">Mark to start from.</param>
    /// <param name="offset">Number of characters, positive or negative, of offset.</param>
    /// <exception cref="ArgumentException">The Mark has been invalidated.</exception>
        public static Mark operator -(Mark mark, long offset)
        {
            mark.Validate("mark");
            return mark.OffsetMark(-offset, true);
        }

    // Do the work of offsetting a Mark, allowing the returned Mark to be invalidatable or not.
        internal Mark OffsetMark(long offset, bool invalidatable)
        {

            TextBlock block = _block;
            int position = _position;

            if (offset>0)
            {
                int blockLen = block.GetLength();

                // Note that we cannot end up at position = 0 unless there only is one empty block
                while (position + offset > blockLen && block.GetNext() != null)
                {
                    offset -= blockLen - position;
                    position = 0;
                    block = block._next;
                    blockLen = block.GetLength();
                }
                if (position+offset<=blockLen) position += (int)offset;
                else position = blockLen;
            }

            else if (offset<0)
            {
                // Note that we cannot end up at position = 0 unless we are in the first block
                while (position + offset <= 0 && block._previous != null)
                {
                    offset += position;
                    block = block._previous;
                    position = block.GetLength();
                }
                if (position+offset > 0) position += (int)offset;
                else position = 0;
            }

            return new Mark(_text, block, position, invalidatable);
        }

    /// <summary><para>Returns an unregistered Range between the two Marks.</para>
    /// <para>Do not assign this value to a Range variable - it will become invalidated if the
    /// text changes.  Use Text.AddRange() or Range.MoveTo() instead.</para></summary>
    /// <param name="mark1">Mark at the start of the Range.</param>
    /// <param name="mark2">Mark at the end of the Range.</param>
    /// <exception cref="ArgumentException">At least one of the Marks has been invalidated, or
    /// the Marks are in different Texts.</exception>
        public static Range operator +(Mark mark1, Mark mark2)
        {
            mark1.Validate("mark1");
            mark2.Validate(mark1._text, "mark2");
            if (mark2 - mark1 < 0)
                throw new ArgumentException("Mark arguments are in the wrong order.",
                                                                                  "mark1, mark2");
            Mark mark1New = new Mark(mark1, true);
            Mark mark2New = new Mark(mark2, true);
            return new Range(mark1New, mark2New);
        }

    /// <summary><para>Returns an unregistered Mark located at the start of the line which the Mark 
    /// is in.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public Mark LineStart()
        {
            ValidateThis();
            return LineStart(true);
        }

    // Do the work of finding the start of the line, allowing the returned Mark to be invalidatable
    // or not.  Note this approach is 3-4 times faster than ToString().LastIndexOf('\n').
        internal Mark LineStart(bool invalidatable)
        {
            TextBlock blockPrev = _block;
            TextBlock block;
            int position = _position;

            do
            {
                block = blockPrev;
                StringBuilder sb = block.GetData();

                while (position>0)
                {
                    if (sb[position-1] == '\n')
                        return new Mark(_text, block, position, invalidatable);
                    position--;
                }

                blockPrev = block._previous;
                if (blockPrev != null) position = blockPrev.GetLength();
            } while (blockPrev != null);

            return new Mark(_text, block, 0, invalidatable); // Beginning of the whole Text
        }

    /// <summary><para>Returns an unregistered Mark located at the end of the line which the Mark is
    /// in.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public Mark LineEnd()
        {
            return LineEnd(true);
        }

    // Do the work of finding the start of the line, allowing the returned Mark to be invalidatable
    // or not.  Note this approach is 3-4 times faster than ToString().IndexOf('\n').
        internal Mark LineEnd(bool invalidatable)
        {
            ValidateThis();

            TextBlock blockNext = _block;
            TextBlock block;
            int position = _position;
            int length;

            do
            {
                block = blockNext;
                StringBuilder sb = block.GetData();
                length = sb.Length;

                while (position<length)
                {
                    if (sb[position] == '\n') 
                        return new Mark(_text, block, position, invalidatable);
                    position++;
                }

                blockNext = block.GetNext();
                if (blockNext != null && blockNext.GetData()[0] == '\n')
                    return new Mark(_text, block, position, invalidatable); // End of this block, not start of next

                position = 0;
            } while (blockNext != null);

            return new Mark(_text, block, length, invalidatable); // End of the whole Text
        }

    /// <summary><para>Returns an unregistered Range containing the line (excluding newline
    /// characters) which the Mark is in.</para>
    /// <para>Do not assign this value to a Range variable - it will become invalidated if the
    /// text changes.  Use Text.AddRange() or Range.MoveTo() instead.</para></summary>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public Range Line()
        {
            return new Range(LineStart(), LineEnd());
        }

    /// <summary>Returns a string showing up to four characters either side of the Mark.</summary>
        public override string ToString()
        {
            return ToString(4);
        }

    /// <summary>Returns a string showing characters either side of the Mark.</summary>
    /// <param name="chars">Number of characters to show on either side.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
        public string ToString(int chars)
        {
            ValidateThis();

            return String.Concat(new Range(OffsetMark(-chars,false), this).ToString(),
                                 "<|>",
                                 new Range(this, OffsetMark(chars,false)).ToString());
        }

    /// <summary><para>Removes the Mark from the list of Marks registered with its Text.</para>
    /// <para>The Mark will be invalidated by subsequent changes to the text.</para></summary>
        public void Remove()
        {
            _text.Remove(this);
        }

    //---------------------------------------------------------------------------------------------

    /// <summary>Returns true if this Mark is at the start of, or within, but not at the end of the
    /// passed Range.</summary>
    /// <param name="range">Range which Mark may lie within.</param>
    /// <exception cref="InvalidOperationException">This Mark has been invalidated.</exception>
    /// <exception cref="ArgumentException">The Range has been invalidated or is in a different
    /// Text.</exception>
        public bool Within(Range range)
        {
            ValidateThis();
            range.Validate(_text, "range");
            return DoWithin(range);
        }

    // Do the work of the Within and Contains functions
        internal bool DoWithin(Range range)
        {
            TextBlock block = range._mark1._block;
            int startPosition = range._mark1._position;
            TextBlock endblock = range._mark2._block;
            int endPosition = range._mark2._position;

        // Assume the range is relatively short
            while (block != endblock)
            {
                if (_block == block && _position >= startPosition) return true;
                block = block._next;
                startPosition = 0;
            }

            if (_block == block && _position >= startPosition && _position < endPosition) return true;
            return false;
        }

    //---------------------------------------------------------------------------------------------

    /// <summary>Text which the Mark is in.</summary>
        public Text Text
        {
            get {return _text;}
        }

    // Used by search to get the character after the Mark if there is one, and move the Mark on
        internal bool NextChar(ref char next)
        {
            if (_position < _block.GetData().Length)
            {
                next = _block._data[_position];
                _position++;
                return true;
            }
            else if (_block.GetNext() != null)
            {
                _block = _block._next;
                next = _block.GetData()[0];
                _position = 1;
                return true;
            }
            else
            {
                return false;
            }
        }

    // Used by search to get the character before the Mark if there is one, and move the Mark back
        internal bool PreviousChar(ref char previous)
        {
            if (_position > 0)
            {
                _position--;
                previous = _block.GetData()[_position];
                return true;
            }
            else if (_block._previous != null)
            {
                _block = _block._previous;
                _position = _block.GetData().Length - 1;
                previous = _block._data[_position];
                return true;
            }
            else
            {
                return false;
            }
        }

    /// <summary>Override for default GetHashCode() - returns an index which is unique for Marks
    /// created with Text.AddMark(). An unregistered Mark returns a value of zero or -1 if it has
    /// been invalidated by a change to the text.</summary>
        public override int GetHashCode()
        {
            return _ID;
        }
    }

}